import pygame
#import pygame._view  ## required for the py2exe script. Uncommented if needed
import random, math
from twisted.protocols import amp
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.internet.endpoints import TCP4ClientEndpoint
from snakecommon import *
from twisted.internet import reactor
import zope.interface
import config

pygame.init()
pygame.font.init()
CLEAR_COLOR     = (255,255,255) ## white
PART_COLOR      = (0,0,0)       ## black
FOOD_COLOR      = (0,0,255)     ## blue
MAX_FPS         = 25
FONT_NAME       = 'PCSeniorRegular.ttf'
FONT_SIZE_BIG   = 24
FONT_SIZE_SMALL = 12
TEXT_COLOR      = (255,0,0)
P1_HEAD_START   = (5,12)
P2_HEAD_START   = (15,12)
FOOD_START      = (10,12)
BIG_FONT        = pygame.font.Font(FONT_NAME,FONT_SIZE_BIG)
SMALL_FONT      = pygame.font.Font(FONT_NAME,FONT_SIZE_SMALL)

class SnakeClient(amp.AMP):
    # Has a 'game' member which references the snake game main class

    def onKick(self,reason):
        print "[In] Kicked for:",reason
        return {}
    Kick.responder(onKick)

    def onAddSession(self,id,lobby_state):
        print "[In] Adding session. ID: %s, lobby state: %s"%(id,lobby_state)
        self.game.addSession(id,lobby_state)
        return {}
    AddSession.responder(onAddSession)

    def onRemoveSession(self,id):
        print "[In] Removing a session. ID: %s"%id
        self.game.removeSession(id)
        return {}
    RemoveSession.responder(onRemoveSession)

    def onLobbyStateChange(self,id,lobby_state):
        print "[In] Peer ID: %s changed lobby state to %s"%(id,lobby_state)
        self.game.sessions[id] = lobby_state
        return {}
    LobbyStateChange.responder(onLobbyStateChange)

    def onChangeProgramState(self,program_state):
        print "[In] Server changed state to %s"%program_state
        self.game.changeProgramState(program_state)
        return {}
    ChangeProgramState.responder(onChangeProgramState)

    def onSetHeading(self,id,new_heading):
        print "[In] Peer ID: %s changed heading to %s"%(id,new_heading)
        self.game.remoteSetHeading(id,new_heading)
        return {}
    SetHeading.responder(onSetHeading)

    def onAddSnakePart(self,id,row,col):
        tup = (row,col)
        print "[In] Peer %s added part: %s"%(id,tup)
        self.game.remoteAddedPart(id,tup)
        return {}
    AddSnakePart.responder(onAddSnakePart)

    def onPlaceFood(self,row,col):
        print "[In] Food placed at (%s,%s)"%(row,col)
        self.game.remotePlacedFood((row,col))
        return {}
    PlaceFood.responder(onPlaceFood)

    def onTick(self,id,row,col):
        print "[In] Tick"
        pos = (row,col)
        self.game.remoteTick(id,pos)
        return {}
    Tick.responder(onTick)

    def connectionLost(self,reason):
        self.game.onDisconnect()
        print "Connection to server lost; why:", reason

class SnakeClientFactory(ReconnectingClientFactory):
    def __init__(self,game):
        self.game = game
    def startedConnecting(self,connector):
        print "Start connecting..."

    def clientConnectionFailed(self,connector,reason):
        print "Connection attempt failed.."
        self.retry()
    def buildProtocol(self,addr):
        print "Building a SnakeClient"
        c = SnakeClient()
        c.game = self.game
        return c

class Display(zope.interface.Interface):
    def init(screen):
        """Initialize things as needed, screen is a pygame.surface"""
    def end():
        """Clean up if needed"""
    def render(self):
        """Draw this display"""

class ConnectingDisplay:
    zope.interface.implements(Display)
    def init(self,screen,game):
        self.screen = screen
        self.game   = game # not used for anything
        self.status_label = BIG_FONT.render("- Connecting -",False,TEXT_COLOR)
        size = self.status_label.get_size()
        self.label_pos = ((TILE_SIZE*H_TILES - size[0])/2,(TILE_SIZE*V_TILES)*3/4)
    def end(self):
        pass
    def render(self):
        self.screen.blit(self.status_label,self.label_pos)

class LobbyDisplay:
    zope.interface.implements(Display)
    def init(self,screen,game):
        self.screen = screen
        self.game   = game
        self.main_label = BIG_FONT.render("Lobby",False,TEXT_COLOR)
        print "Initializing lobby display"

    def end(self):
        pass
    def render(self):
        label_size  = self.main_label.get_size()
        l1_pos      = ((TILE_SIZE*H_TILES - label_size[0])/2,(TILE_SIZE*V_TILES)*1/4)
        self.screen.blit(self.main_label,l1_pos)

        my_state  = self.game.sessions[self.game.id]
        if len(self.game.sessions) > 1:
            other_sessions = dict(self.game.sessions)
            other_sessions.pop(self.game.id)
            opp_state = other_sessions.values()[0] ## fucking ugly!
        else:
            opp_state = LobbyState.WAITING

        opp_ready_text = {LobbyState.NOT_READY: "Opponent not ready.",
                          LobbyState.READY:     "Opponent is ready.",
                          LobbyState.WAITING:   "Waiting on opponent"}
        label_text = opp_ready_text[opp_state]
        opp_label  = SMALL_FONT.render(label_text,False,TEXT_COLOR)
        label_size = opp_label.get_size()
        label_pos = ((TILE_SIZE*H_TILES - label_size[0])/2,(TILE_SIZE*V_TILES)*2/4)
        self.screen.blit(opp_label,label_pos)

        ready_text = {LobbyState.NOT_READY: "You're not ready. (Push space)",
                      LobbyState.READY:     "You are ready!"}

        label_text = ready_text[my_state]
        ready_label = SMALL_FONT.render(label_text,False,TEXT_COLOR)
        label_size = ready_label.get_size()
        label_pos = ((TILE_SIZE*H_TILES - label_size[0])/2,(TILE_SIZE*V_TILES)*3/4)
        self.screen.blit(ready_label,label_pos)

class GameDisplay:
    zope.interface.implements(Display)
    def init(self,screen,game):
        self.screen = screen
        self.game   = game
        print "Initializing game display"
    def end(self):
        pass
    def render(self):
        for snake in self.game.snakes.values():
            for part in snake.parts:
                x,y = part[0]*TILE_SIZE,part[1]*TILE_SIZE
                pygame.draw.rect(self.screen,PART_COLOR, (x,y,TILE_SIZE,TILE_SIZE) )

        food_x = self.game.food[0]*TILE_SIZE
        food_y = self.game.food[1]*TILE_SIZE
        pygame.draw.rect(self.screen,FOOD_COLOR,(food_x,food_y,TILE_SIZE,TILE_SIZE))

class PygameEventHandler(zope.interface.Interface):
    def init(game):
        """Pass the game object to the handler.."""
    def end():
        """Clean up, if needed"""
    def processEvents():
        """Pump the pygame event queue and handle events"""

class ConnectingPygameEventHandler:
    zope.interface.implements(PygameEventHandler)
    def init(self,game):
        self.game = game
    def end(self):
        pass # Nothing to do
    def processEvents(self):
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.game.state = ProgramState.EXITING

class LobbyPygameEventHandler:
    zope.interface.implements(PygameEventHandler)
    def init(self,game):
        self.game= game
        print "Initializing Lobby Event Handler"
    def end(self):
        pass # Nothing to do
    def processEvents(self):
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.game.state = ProgramState.EXITING
                elif event.key == pygame.K_SPACE:
                    self.game.toggleReadyState()

class GamePygameEventHandler:
    zope.interface.implements(PygameEventHandler)
    def init(self,game):
        self.game = game
        print "Initializing Game Event Hander"
    def end(self):
        pass
    def processEvents(self):
        events = pygame.event.get()
        for event in events:
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    self.game.state = ProgramState.EXITING
                elif event.key in [pygame.K_w,pygame.K_a,pygame.K_s,pygame.K_d]:
                    self.processMovement(event.key)

    def processMovement(self,key):
        heading_map = {pygame.K_w:Heading.UP,   pygame.K_a:Heading.LEFT,
                       pygame.K_s:Heading.DOWN, pygame.K_d:Heading.RIGHT}
        heading = heading_map[key]

        ## a one-part snake can move in any direction it damn pleases

        my_id = self.game.id
        if len(self.game.snakes[my_id].parts)  < 2:
            self.game.setNewHeading(heading)
        else:
            print "Current heading: ",self.game.snakes[my_id].heading
            ## old_heading:(tuple of possible new headings changes)
            valid_new_headings = {Heading.DOWN: (Heading.LEFT,Heading.RIGHT),
                                  Heading.LEFT: (Heading.UP,  Heading.DOWN),
                                  Heading.UP:   (Heading.LEFT,Heading.RIGHT),
                                  Heading.RIGHT:(Heading.UP,  Heading.DOWN)}
            print "Permissible headings: ",valid_new_headings[self.game.snakes[my_id].heading]

            if heading in valid_new_headings[self.game.snakes[my_id].heading]:
                self.game.setNewHeading(heading)

class Snake:
    def __init__(self):
        self.parts   = []
        self.heading = Heading.DOWN

class Game:
    def init(self):
        self.state           = ProgramState.CONECTING
        self.sessions = {} # Dictionary of <id>:<lobby state>
        self.protocol = None
        self.id       = None

        self.screen   = pygame.display.set_mode( (TILE_SIZE*H_TILES,TILE_SIZE*V_TILES))
        self.display  = None
        self.changeDisplay(ConnectingDisplay)

        self.event_handler = None
        self.changeEventHandler(ConnectingPygameEventHandler)

        self.factory = SnakeClientFactory(self)
        self.endpoint = TCP4ClientEndpoint(reactor,config.ip,config.port)
        self.tryConnecting()
        reactor.callLater(1/MAX_FPS,self.update)

        ## Game related stuff here
        self.snakes = {} # Dictionary of <id>:<snake>
        self.food        = []

    def toggleReadyState(self):
        if self.sessions[self.id] == LobbyState.NOT_READY:
            new_state = LobbyState.READY
        else:
            new_state = LobbyState.NOT_READY

        self.sessions[self.id] = new_state
        self.protocol.callRemote(LobbyStateChange,id=self.id,lobby_state=new_state)

    def addSession(self,id,lobby_state):
        self.sessions[id] = lobby_state

    def removeSession(self,id):
        if id in self.sessions:
            self.sessions.pop(id)

    def tryConnecting(self):
        d = self.endpoint.connect(self.factory)
        d.addCallback(self.onConnect)
        d.addErrback(self.onConnectFailure)

    def changeEventHandler(self,EventHandlerClassType):
        if self.event_handler:
            self.event_handler.end()
        self.event_handler = EventHandlerClassType()
        self.event_handler.init(self)

    def changeDisplay(self,DisplayClassType):
        if self.display:
            self.display.end()
        self.display = DisplayClassType()
        self.display.init(self.screen,self)

    def draw(self):
        self.screen.fill(CLEAR_COLOR)
        self.display.render()
        pygame.display.flip()

    def update(self):
        self.event_handler.processEvents()
        self.draw()
        if not self.state == ProgramState.EXITING:
            reactor.callLater(1/MAX_FPS,self.update)
        else:
            reactor.stop()

    def onHandshakeSuccess(self,response):
        self.id = response['session_id']
        self.changeDisplay(LobbyDisplay)
        self.changeEventHandler(LobbyPygameEventHandler)
        print "Connected as ID: %d"%self.id

    def onHandshakeFailure(self,err):
        print "Handshake failure. Why?: %s"%err
        self.state = ProgramState.EXITING

    def onConnect(self,protocol):
        self.protocol = protocol
        d = self.protocol.callRemote(Handshake)
        d.addCallback(self.onHandshakeSuccess)
        d.addErrback(self.onHandshakeFailure)
        print "[Out] Handshaking"

    def onConnectFailure(self,err):
        print "Failed to connect. Why?: %s. Reconnecting.."%err
        self.tryConnecting()

    def onDisconnect(self):
        print "Disconnected! Reconnecting..."
        self.changeDisplay(ConnectingDisplay)
        self.changeEventHandler(ConnectingPygameEventHandler)
        self.tryConnecting()

    def changeProgramState(self,program_state):
        self.state = program_state
        if program_state == ProgramState.GAME:
            self.changeDisplay(GameDisplay)
            self.changeEventHandler(GamePygameEventHandler)
            self.startGame()
        else:
            self.changeDisplay(LobbyDisplay)
            self.changeEventHandler(LobbyPygameEventHandler)
            self.endGame()

    def startGame(self):
        self.snakes = {}
        # some default values so nothing explodes
        for session_id in self.sessions.keys():
            self.snakes[session_id] = Snake()

        self.food        = (0,0)

    def endGame(self):
        pass

    def remoteAddedPart(self,id,part):
        self.snakes[id].parts.insert(0,part)

    def remotePlacedFood(self,pos):
        self.food = pos

    def setNewHeading(self,heading):
        self.protocol.callRemote(SetHeading,id=self.id,new_heading=heading)
        print "[Out] Setting new heading to %s"%heading

    def remoteSetHeading(self,id,heading):
        # opponent heading set here..
        self.snakes[id].heading = heading

    def remoteTick(self,id,next_coordinate):
        self.snakes[id].parts.insert(0,next_coordinate)
        self.snakes[id].parts.pop(-1)

def main():
    game = Game()
    game.init()
    reactor.run()

if __name__ == '__main__':
    main()